/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.server.virtual.controller;

import offset.nodes.server.dispatch.controller.DispatchAttribute;
import offset.nodes.server.virtual.controller.dispatch.VirtualDispatchAttribute;
import java.io.ByteArrayInputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Properties;
import java.util.Stack;
import java.util.StringTokenizer;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
import javax.jcr.ValueFormatException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import offset.nodes.server.controller.*;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Value;
import javax.xml.transform.TransformerConfigurationException;
import offset.nodes.Constants;
import offset.nodes.client.virtual.model.SimpleQuery;
import offset.nodes.client.model.HttpUtils;
import offset.nodes.client.virtual.model.jcr.QName;
import offset.nodes.client.virtual.model.jcr.SimpleNode;
import offset.nodes.client.editor.model.HyperlinkModel;
import offset.nodes.client.veditor.model.DataModel;
import offset.nodes.client.veditor.model.SecondaryQuery;
import offset.nodes.client.veditor.model.VirtualHyperlinkModel;
import offset.nodes.server.version.model.FrozenNode;
import offset.nodes.server.html.model.HttpContext;
import offset.nodes.server.virtual.model.sax.ElementContentSource;
import offset.nodes.server.virtual.model.sax.UpdatableSAXContentSource;
import offset.nodes.server.virtual.model.VirtualModel;
import offset.nodes.server.workflow.controller.WorkflowAttribute;
import org.apache.log4j.Logger;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.xml.sax.SAXException;

/**
 *
 * @author Walter L�tgenau
 */
public class VirtualAction extends AuthenticatingAction {

    private static Logger log = Logger.getLogger(RepositoryPlugIn.class);

    /**
     * For the path provided, try to find the virtual instance. 
     * 
     * For that, first find the last existing child, starting from the path parent, that matches part of the path. If the path provided is a path ending
     * in virtual items, the last existing child will contain a virtualPageInstance or, more frequently, a virtualFolderInstance.
     * 
     * @param model the VirtualModel used to find the last existing child
     * @param path the path
     * @return the parent virtual instance child.
     */
    protected Node findTemplateInstance(VirtualModel model, String path) {
        try {
            Node node = model.findLastExistingNode(path);
            if (node.isNodeType(Constants.TYPE_TEMPLATE_INSTANCE)) {
                return node;
            }
            return null;
        } catch (Exception e) {
            log.error(VirtualAction.class.getName(), e);
            return null;
        }
    }
    
    protected ActionForward processVirtualHyperlink(VirtualModel model, ActionMapping mapping, VirtualForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
        String ref = null;
        String vref = null;
        DispatchAttribute dispatch = (DispatchAttribute) request.getAttribute(DispatchAttribute.ATT_DISPATCH);

        // part of the request parameters
        if ( form.getRef() != null) {
            ref = form.getRef();
            vref = form.getVref();
        } else if (request.getAttribute(VirtualDispatchAttribute.ATT_VIRTUAL_DISPATCH) != null) { // default template instance
            VirtualDispatchAttribute virtual = (VirtualDispatchAttribute) request.getAttribute(VirtualDispatchAttribute.ATT_VIRTUAL_DISPATCH);

            if (!dispatch.getNode().isNodeType(Constants.JCR_REFERENCEABLE))
                return null;
            ref = dispatch.getNode().getUUID();
            vref = virtual.getVref();

            // if this is a multipage node, use the configured template, if present
            if (dispatch.getNode().isNodeType(Constants.TYPE_ABSTRACT_MULTIPAGE)
                    && dispatch.getNode().hasProperty(Constants.PROP_EDITOR_VIRTUAL_PAGE_INSTANCE))
                vref = dispatch.getNode().getProperty(Constants.PROP_EDITOR_VIRTUAL_PAGE_INSTANCE).getString();
        }
        
        Node data = model.getReferencedNode(ref);
        if (data != null)
            dispatch.setPath(data.getPath());

        // Special case for multipages after edit only: Return to display page, not to edit page
        if (data != null
                && data.isNodeType(Constants.TYPE_ABSTRACT_MULTIPAGE)
                && data.hasProperty(Constants.PROP_EDITOR_VIRTUAL_PAGE_INSTANCE)
                && form.getResult() != null /* must have added by editor */)
            vref = data.getProperty(Constants.PROP_EDITOR_VIRTUAL_PAGE_INSTANCE).getString();

        if (vref == null)
            vref = model.getDefaultTemplateUUID(data.getPrimaryNodeType().getName());
        Node templateInstance = model.getReferencedNode(vref);
        
        if ( data == null || templateInstance == null)
            return null;

        if (data.isNodeType(Constants.NT_FROZEN_NODE))
            data = new FrozenNode(data);
        configureVirtualPage(request, response, model, data, templateInstance, form);
        
        return forwardToPage(request, form, mapping);
    }

     public ActionForward process(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
        VirtualForm virtualForm = (VirtualForm) form;
        VirtualModel model = new VirtualModel(getRepositorySession(request));

        if ( virtualForm.getRef() != null || request.getAttribute(VirtualDispatchAttribute.ATT_VIRTUAL_DISPATCH) != null)
            return processVirtualHyperlink(model, mapping, virtualForm, request, response);

        String path = getPath(request);

        Node templateInstance = findTemplateInstance(model, path);
        if (templateInstance == null) {
            return null;
        }

        int start = path.indexOf(templateInstance.getPath());
        if (start >= 0 && start < path.length()) {
            path = path.substring(start + templateInstance.getPath().length());
        }

        Node matchingNode = null;
        String[] pathComponents = path.split("/");
        PathNodes pathNodes = new PathNodes();

        Node parent = templateInstance;
        for (int i = 0; i < pathComponents.length; i++) {
            matchingNode = findMatchingNode(model, parent, pathComponents[i], pathNodes);
            if (matchingNode != null) {
                parent = matchingNode;
                continue;
            } else {
                Node potentiallyMatchingNode = findPotentiallyMatchingNode(parent, pathComponents[i]);
                if (potentiallyMatchingNode != null) {
                    configureCreationPage(request, response, getPath(request), model, potentiallyMatchingNode, pathNodes, pathComponents[i], virtualForm);
                    return forwardToPage(request, virtualForm, mapping);
                } else {
                    return mapping.findForward("error");
                } // TODO: add information about the erroneous path component
            }
        }

        if (matchingNode.isNodeType(Constants.TYPE_TEMPLATE_INSTANCE) || matchingNode.isNodeType(Constants.TYPE_VIRTUAL_FOLDER_INSTANCE)) {
            configureVirtualFolder(request, model, getPath(request), pathNodes, matchingNode);

            return mapping.findForward("folder");
        } else if (matchingNode.isNodeType(Constants.TYPE_VIRTUAL_PAGE_INSTANCE)) {
            configureVirtualPage(request, response, getPath(request), model, pathNodes, matchingNode, virtualForm);

            return forwardToPage(request, virtualForm, mapping);
        }

        return null;
    }

    /**
     * Forward to the page. 
     * 
     * If the URL path request has come from an applet, the page will be the applet page, else the html page.
     * 
     * @param form used to determine the source of the request
     * @param mapping used to obtain the forward
     * @return the resulting forward.
     */
    protected ActionForward forwardToPage(HttpServletRequest request, VirtualForm form, ActionMapping mapping) {
        ActionForward forward;
        if (form.getMode().equals(Constants.PARVALUE_APPLET)) {
            forward = mapping.findForward("applet");
        } else if (form.getMode().equals(Constants.PARVALUE_PRINT)) {
            forward = mapping.findForward("print");
        } else if (request.getAttribute(EditorAttribute.ATT_EDITOR) != null) {
            forward = mapping.findForward("edit");
        }
        else {
            forward = mapping.findForward("browser");
        }

        return forward;

    }

    /**
     * From the child nodes of the given parent, check if any of the child names matches the given pathComponent.
     * 
     * Each child child will be a virtualFolderInstance or a virtualPageInstance child. Both types of nodes may have either a fixed name, or
     * a query and a nameProperty. 
     * 
     * In the former case, just compare the fixed name with the pathComponent. If they match, return the virtual child.
     * 
     * In the latter case, first execute the query. Then, for every buf child, retrieve the nameProperty. If the buf matches the pathComponent, 
     * return the virtual child.
     * 
     * @param model utility for access to the repository
     * @param parent The parent, the children of which will be checked for the pathComponent
     * @param pathComponent The part of the URL path provided by the user, that is associated to the parents children
     * @param pathNodes 
     * @return the virtual instance child.
     * @throws javax.jcr.RepositoryException 
     */
    protected Node findMatchingNode(VirtualModel model, Node parent, String pathComponent, PathNodes pathNodes) throws RepositoryException {
        if (pathComponent.length() == 0) {
            return parent;
        }

        NodeIterator i = parent.getNodes();
        while (i.hasNext()) {
            Node child = i.nextNode();
            if (child.hasProperty(Constants.PROP_NAME)) {
                if (child.getProperty(Constants.PROP_NAME).getString().equals(pathComponent)) {
                    return child;
                }
            } else if (child.hasProperty(Constants.PROP_NAME_PROPERTY) && child.hasProperty(Constants.PROP_PRIMARY_QUERY)) {
                String nameProperty = child.getProperty(Constants.PROP_NAME_PROPERTY).getString();
                QueryParameters parameters = expandQuery(pathNodes, child.getProperty(Constants.PROP_PRIMARY_QUERY).getString());
                NodeIterator result = model.executeQuery(parameters.getQuery());
                HashMap children = new HashMap();
                while (result.hasNext()) {
                   Node data = result.nextNode();
                    
                    // get node name.
                    String childName = null;
                    // if the name property is a residual property, use the name of the data node
                    if (nameProperty.equals(QName.NAME_PROPERTY.getLocalName()))
                        childName = data.getName();
                    // if the name property is an ordinary property, use its value;
                    else if (data.hasProperty(nameProperty))
                        childName = data.getProperty(nameProperty).getString();
                    
                    // if a node name could be retrieved
                    if (childName != null) {
                        if ( children.containsKey(childName)) {
                            Integer index = (Integer) children.get(childName);
                            childName = childName + "[" + (index.intValue()+1) + "]";
                            children.put(childName, new Integer(index.intValue()+1));
                        }
                        else
                            children.put(childName, new Integer(1));
                        if (childName.equals(pathComponent)) {
                            pathNodes.add(data);
                            return child;
                        }
                    }

                }

            }
        }

        return null;
    }

    /**
     * If no matching child has been found, this method can be used for nodes, that might match, provided that the queried data will be updated.
     * 
     * These are nodes, that have a query and a nameProperty associated to them. When the first such child has been found, return it.
     * 
     * @param parent The parent, where the potentially matching child will be one of its children.
     * @param pathComponent The part of the path, that might match.
     * @return the potentially matching child
     * @throws javax.jcr.RepositoryException 
     */
    protected Node findPotentiallyMatchingNode(Node parent, String pathComponent) throws RepositoryException {
        NodeIterator ni = parent.getNodes();
        while (ni.hasNext()) {
            Node child = ni.nextNode();
            if (child.isNodeType(Constants.TYPE_VIRTUAL_INSTANCE) &&
                    child.hasProperty(Constants.PROP_PRIMARY_QUERY) &&
                    child.hasProperty(Constants.PROP_NAME_PROPERTY)) {
                return child;
            }
        }

        return null;
    }

    /**
     * When a potentially matching child has been found, we try to find the virtual page, that can be used to add the missing data.
     * 
     * From the query of the potentially matching child we determine the data nodeType, for which an entry corresponding to the requested path
     * component is missing. From all virtualPages of the template we search for one, that is associated to this nodeType. If found, we return it.
     * 
     * @param model the repository model, supporting with general interest methods
     * @param potentiallyMatchingNode The potentially matching child with the query.
     * @return The templatePageNode, that is associated to the nodeType of the query.
     * @throws javax.jcr.RepositoryException 
     */
    protected Node findConfiguringPage(VirtualModel model, Node potentiallyMatchingNode) throws RepositoryException {
        if (!potentiallyMatchingNode.hasProperty(Constants.PROP_TEMPLATE_NODE_REFERENCE)) {
            return null;
        }

        Node templateNode = potentiallyMatchingNode.getProperty(Constants.PROP_TEMPLATE_NODE_REFERENCE).getNode();
        Node representation = templateNode.getNode(Constants.CHILD_REPRESENTATION);
        String nodeTypeName = representation.getProperty(Constants.PROP_NODETYPE).getString();

        Node templateNodeRoot = templateNode;
        while (templateNode.isNodeType(Constants.TYPE_VIRTUAL_INSTANCE)) {
            templateNodeRoot = templateNode;
            templateNode = templateNode.getParent();
        }

        Node virtualPage = model.findTypeWithProperty(templateNodeRoot, Constants.TYPE_VIRTUAL_PAGE, Constants.PROP_NODETYPE, nodeTypeName);
        return virtualPage;
    }
    
    protected boolean isNumber(String number) {
        try {
            Integer.parseInt(number);
        }
        catch( NumberFormatException e) {
            return false;
        }
        
        return true;
    }
    
    protected String discardFilters(String path) {
        final int IDLE = 0;
        final int IN_FILTER = 1;        
        int state = IDLE;
        String filter = null;
        
        StringBuffer result = new StringBuffer();
        StringTokenizer tokenizer = new StringTokenizer(path, "[]", true);
        while(tokenizer.hasMoreTokens()) {
            String part = tokenizer.nextToken();
            
            if ( part.equals("["))
                state = IN_FILTER;
            else if ( part.equals( "]")) {
                state = IDLE;
                if ( isNumber( filter ) ) {
                    result.append( "[" + filter + "]");
                }
            }
            else if ( state == IN_FILTER )
                filter = part;
            else
                result.append(part);
        }
        
        return result.toString();
    }
    
    protected String parentOf(String path) {
        final String PATH_DELIMITER = "/";
        int last = -1;
        if ( (last = path.lastIndexOf(PATH_DELIMITER)) < 0)
            return path;
        return path.substring(0, last);
    }
    
    /**
     * This will be used, if the query for a creation node of the topmost level shall be expanded. In that case, no path component has been
     * resolved yet. The creation node will be a virtualPage, but the a higher virtualFolder node may have the exact query without "ancestor()" 
     * part.
     * 
     * @param templateInstanceNode
     * @param nodeType
     * @return
     * @throws javax.jcr.PathNotFoundException
     * @throws javax.jcr.ValueFormatException
     * @throws javax.jcr.RepositoryException
     */
    protected String getQueryRoot( Node templateInstanceNode, String nodeType ) throws PathNotFoundException, ValueFormatException, RepositoryException {
        if ( !templateInstanceNode.hasProperty(Constants.PROP_PRIMARY_QUERY) || !templateInstanceNode.hasProperty(Constants.PROP_NODETYPE))
            return getQueryRoot( templateInstanceNode.getParent(), nodeType);
        
        String templateInstanceNodeType = templateInstanceNode.getProperty(Constants.PROP_NODETYPE).getString();
        if ( !nodeType.equals(templateInstanceNodeType))
            return getQueryRoot( templateInstanceNode.getParent(), nodeType);
        
        String query = templateInstanceNode.getProperty(Constants.PROP_PRIMARY_QUERY).getString();
        if ( query.indexOf(Constants.QUERY_ANCESTOR) >= 0)
            return getQueryRoot( templateInstanceNode.getParent(), nodeType );
        
        return parentOf(discardFilters(query));
    }

    /**
     * After finding the creation page, we will retrieve the path of the corresponding transformation and the name of the corresponding nodeType and 
     * add both to the form. 
     * 
     * These will be used as applet parameters and the applet will use this to retrieve the actual values, once the user invokes the "edit" button.
     * 
     * @param model the repository model, supporting with general interest methods
     * @param creationPage
     * @param namePropertyName the key of the name property
     * @param namePropertyValue the value of the name property
     * @param form the VirtualForm
     * @throws javax.jcr.PathNotFoundException
     * @throws javax.jcr.RepositoryException
     * @throws javax.xml.transform.TransformerConfigurationException
     * @throws org.xml.sax.SAXException 
     */
    protected void configureCreationPage(HttpServletRequest request, HttpServletResponse response, String path, VirtualModel model, Node templateInstanceNode, PathNodes pathNodes, String namePropertyValue, VirtualForm form) throws PathNotFoundException, RepositoryException, TransformerConfigurationException, SAXException {
       Node creationPage = findConfiguringPage(model, templateInstanceNode);
       if (!creationPage.hasProperty(Constants.PROP_NODETYPE) || !creationPage.hasNode(Constants.CHILD_TRANSFORMATION)) {
            return;
        }

        String query = templateInstanceNode.getProperty(Constants.PROP_PRIMARY_QUERY).getString();
        String nodeTypeName = creationPage.getProperty(Constants.PROP_NODETYPE).getString();
        String transformationPath = creationPage.getNode(Constants.CHILD_TRANSFORMATION).getPath();
        QueryParameters queryParameters = expandQuery(pathNodes, query);
        String queryRoot = null;
        if ( queryParameters.getQuery() != null)
            queryRoot = parentOf(queryParameters.getQuery()); // if already a part of the name has been resolved
        else
            queryRoot = getQueryRoot(templateInstanceNode, nodeTypeName); // if no part has been resolved. In that case, find the root and substitute it.

        Properties attributes = new Properties();
        String namePropertyName = creationPage.getProperty(Constants.PROP_NAME_PROPERTY).getString();
        attributes.setProperty(namePropertyName, namePropertyValue);
        ElementContentSource data = new ElementContentSource(nodeTypeName, attributes);
        
        String page = model.createMappedHtml(transformationPath, nodeTypeName, queryRoot, data, new HttpContext(getServlet(), request, response));

        form.setHtml(new ByteArrayInputStream(page.getBytes()));
        
        VirtualAttribute att = new VirtualAttribute();
        att.setContent(page);
        att.setPath(path);
        if (creationPage.hasNode(Constants.CHILD_CSS))
            att.setCssPath(creationPage.getPath()+"/"+Constants.CHILD_CSS);
        att.setInstancePath(templateInstanceNode.getPath());
        if (form.getDir() != null)
            att.setParentPath(form.getDir());
        
        request.setAttribute(VirtualAttribute.ATT_VIRTUAL, att);
    }

    class QueryParameters {

        String query = null;
        String root = null;

        public QueryParameters() {
        }

        public QueryParameters(String query, String root) {
            this.query = query;
            this.root = root;
        }

        public String getQuery() {
            return query;
        }

        public void setQuery(String query) {
            this.query = query;
        }

        public String getRoot() {
            return root;
        }

        public void setRoot(String root) {
            this.root = root;
        }
    }

    class PathNodes {

        Stack nodes = new Stack();
        HashSet nodeSet = new HashSet();

        public void add(Node node) {
            if (nodeSet.contains(node)) {
                return;
            }
            nodeSet.add(node);
            nodes.push(node);
        }
        
        public int size() {
            return nodeSet.size();
        }

        public Node findAncestorOfType(String nodeType) throws RepositoryException {
            for (int i = nodes.size() - 1; i >= 0; i--) {
                Node node = (Node) nodes.get(i);
                if (node.isNodeType(nodeType)) {
                    return node;
                }
            }

            return null;
        }

        public Node findAncestorOfName(String name) throws RepositoryException {
            for (int i = nodes.size() - 1; i >= 0; i--) {
                Node node = (Node) nodes.get(i);
                if (node.getName().equals(name)) {
                    return node;
                }
            }

            return null;
        }
    }
    
    /**
     * Substitute parts of the query of the pattern "<type>[ancestor()]" with the path of the 
     * nearest ancestor of the given type.
     * 
     * @param pathNodes the stack of nodes, used to find the ancestor
     * @param query the query with pattern to be substituted
     * @return the substituted path
     * @throws javax.jcr.RepositoryException
     */
    protected QueryParameters expandQuery(PathNodes pathNodes, String query) throws RepositoryException {
        QueryParameters parameters = expandAncestor(pathNodes, query);
        if ( parameters.getRoot().length() > 0)
            return parameters;
        return expandElement(pathNodes, query);
    }
    
    /**
     * Substitute parts of the query of the pattern "element(*,<type>)" with the path of the 
     * nearest ancestor of the given type.
     * 
     * @param pathNodes the stack of nodes, used to find the ancestor
     * @param query the query with pattern to be substituted
     * @return the substituted path
     * @throws javax.jcr.RepositoryException
     */
    protected QueryParameters expandElement(PathNodes pathNodes, String query) throws RepositoryException {
        if (query.indexOf(Constants.QUERY_ELEMENT) < 0 || pathNodes.size() == 0) {
            return new QueryParameters(query, "");
        }

        StringBuffer buf = new StringBuffer();
        QueryParameters result = new QueryParameters();

        String[] pathComponents = query.split("/");
        boolean ancestorFound = false;
        for (int i = 0; i < pathComponents.length; i++) {
            int startIndex = pathComponents[i].indexOf(Constants.QUERY_ELEMENT);
            if (startIndex >= 0) { 
                /*
                 * Substitute the pattern with a node of the given type, that has been found already
                 */
                int startType = pathComponents[i].indexOf(",", startIndex);
                if ( startType < 0 )
                    continue; // something wrong
                startType++; // skip the comma
                String type = pathComponents[i].substring(startType, pathComponents[i].indexOf(")"));
                Node node = pathNodes.findAncestorOfType(type);
                if (node != null) {
                    ancestorFound = true;
                    buf.append(node.getPath());
                    if ( node.getIndex() == 1)
                        /*
                         * If the index is higher, it has been part of node.getPath() already. For a path "name" and "name[1]" are equivalent, and
                         * "getPath" returns the shorter version. For a query, however, they are not equivalent. "name" will mean all nodes of the given
                         * level. So it is necessary to add the index even for nodes of index 1;
                         */
                        buf.append("[1]"); 
                    result.setRoot(node.getParent().getPath());
                }
            } else if (ancestorFound) {
                /*
                 * Check, if a node of the given name has been found already. Usually, this will be the case, as the path component of the virtualPage, 
                 * that corresponds to the query, has been resolved before, if it already exists.
                 */
                Node node = pathNodes.findAncestorOfName(pathComponents[i]);
                if (node != null) {
                    buf.append(pathComponents[i] + "[" + node.getIndex() + "]");
                } else {
                    /*
                     * This will be the case for components, that are not associated to a data node, e.g. virtualFolders of a fixed name. Such nodes should
                     * be present only once, so it is not necessary to add an index indication.
                     */
                    buf.append(pathComponents[i]);
                }
            }

            if (ancestorFound && i < pathComponents.length - 1) {
                buf.append("/");
            }
        }

        result.setQuery(buf.toString());
        return result;
    }

    /**
     * Substitute parts of the query of the pattern "<type>[ancestor()]" with the path of the 
     * nearest ancestor of the given type.
     * 
     * @param pathNodes the stack of nodes, used to find the ancestor
     * @param query the query with pattern to be substituted
     * @return the substituted path
     * @throws javax.jcr.RepositoryException
     */
    protected QueryParameters expandAncestor(PathNodes pathNodes, String query) throws RepositoryException {
        if (query.indexOf(Constants.QUERY_ANCESTOR) < 0 || pathNodes.size() == 0) {
            return new QueryParameters(query, "");
        }

        StringBuffer buf = new StringBuffer();
        QueryParameters result = new QueryParameters();

        String[] pathComponents = query.split("/");
        boolean ancestorFound = false;
        for (int i = 0; i < pathComponents.length; i++) {
            if (pathComponents[i].indexOf(Constants.QUERY_ANCESTOR) >= 0) { 
                /*
                 * Substitute the pattern with a node of the given type, that has been found already
                 */
                String type = pathComponents[i].substring(0, pathComponents[i].indexOf("["));
                Node node = pathNodes.findAncestorOfType(type);
                if (node != null) {
                    ancestorFound = true;
                    buf.append(node.getPath());
                    if ( node.getIndex() == 1)
                        /*
                         * If the index is higher, it has been part of node.getPath() already. For a path "name" and "name[1]" are equivalent, and
                         * "getPath" returns the shorter version. For a query, however, they are not equivalent. "name" will mean all nodes of the given
                         * level. So it is necessary to add the index even for nodes of index 1;
                         */
                        buf.append("[1]"); 
                    result.setRoot(node.getParent().getPath());
                }
            } else if (ancestorFound) {
                /*
                 * Check, if a node of the given name has been found already. Usually, this will be the case, as the path component of the virtualPage, 
                 * that corresponds to the query, has been resolved before, if it already exists.
                 */
                Node node = pathNodes.findAncestorOfName(pathComponents[i]);
                if (node != null) {
                    buf.append(pathComponents[i] + "[" + node.getIndex() + "]");
                } else {
                    /*
                     * This will be the case for components, that are not associated to a data node, e.g. virtualFolders of a fixed name. Such nodes should
                     * be present only once, so it is not necessary to add an index indication.
                     */
                    buf.append(pathComponents[i]);
                }
            }

            if (ancestorFound && i < pathComponents.length - 1) {
                buf.append("/");
            }
        }

        result.setQuery(buf.toString());
        return result;
    }

    /**
     * The last path component was associated to the template instance or to a virtual folder. In the latter case, the data, that matches the name, exists.
     * 
     * Create a SimpleNode as parent. For each child child, add one or more buf nodes to the parent. There will be one buf child,
     * if the virtual instance child child has no query, but a name. If the child child has a query, however, we will execute the query first. For each
     * buf child of the query, we will add a SimpleNode child to the parent.
     * 
     * @param model 
     * @param path 
     * @param pathNodes 
     * @param virtualFolder The parent, whose child nodes shall be retrieved
     * @throws javax.jcr.RepositoryException
     */
    protected void configureVirtualFolder(HttpServletRequest request, VirtualModel model, String path, PathNodes pathNodes, Node virtualFolder) throws RepositoryException {
        SimpleNode parent = new SimpleNode(path);

        NodeIterator i = virtualFolder.getNodes();
        while (i.hasNext()) {
            Node child = i.nextNode();
            if (child.hasProperty(Constants.PROP_NAME)) {
                if ( child.hasProperty(Constants.PROP_NODETYPE))
                    parent.addNode(child.getProperty(Constants.PROP_NAME).getString(), child.getProperty(Constants.PROP_NODETYPE).getString());
                else
                    parent.addNode(child.getProperty(Constants.PROP_NAME).getString());
            } else if (child.hasProperty(Constants.PROP_NAME_PROPERTY) && child.hasProperty(Constants.PROP_PRIMARY_QUERY)) {
                String nameProperty = child.getProperty(Constants.PROP_NAME_PROPERTY).getString();
                String query = child.getProperty(Constants.PROP_PRIMARY_QUERY).getString();
                QueryParameters parameters = expandQuery(pathNodes, query);
                NodeIterator result = model.executeQuery(parameters.getQuery());
                HashMap children = new HashMap();
                while (result.hasNext()) {
                    Node data = result.nextNode();
                    
                    // get node name.
                    String childName = null;
                    // if the name property is a residual property, use the name of the data node
                    if (nameProperty.equals(QName.NAME_PROPERTY.getLocalName()))
                        childName = data.getName();
                    // if the name property is an ordinary property, use its value;
                    else if (data.hasProperty(nameProperty))
                        childName = data.getProperty(nameProperty).getString();
                    
                    // if a node name could be retrieved
                    if (childName != null) {
                        if ( children.containsKey(childName) ) {
                            Integer childIndex = (Integer) children.get(childName);
                            childName = childName + "[" + (childIndex.intValue()+1) + "]";
                            children.put(childName, new Integer(childIndex.intValue()+1));
                        }
                        else
                            children.put(childName, new Integer(1));
                        parent.addNode(childName, child.getProperty(Constants.PROP_NODETYPE).getString());
                    }
                }
            }
        }

        DispatchAttribute att = new DispatchAttribute();
        att.setNode(parent);
        att.setPath(path);
        request.setAttribute(DispatchAttribute.ATT_DISPATCH, att);
    }
    
    protected String extractId(String html) {
        String id = null;
        int startDiv = html.indexOf("<div");
        if ( startDiv < 0)
            return id;
        int endDiv = html.substring(startDiv).indexOf(">");
        String div = html.substring(startDiv, endDiv);
        int startId = div.indexOf(DataModel.ATTRIBUTE_NAME_ID);
        if ( startId < 0)
            return id;
        
        int startIdValue = startId + DataModel.ATTRIBUTE_NAME_ID.length() + 2;
        int endIdValue = div.substring(startIdValue).indexOf("\"");
        
        id = div.substring(startIdValue, startIdValue+endIdValue);
        
        return id;
    }

    /**
     * The last path component was associated to a virtual page.
     * 
     * From the template, retrieve the transformation and the child type information, add both to the form. From the instance, retrieve the primary query
     * and all secondary queries and add them to the form.
     * 
     * These will be used as applet parameters (secondary queries in the form of "secondary1", "secondary2" etc.). The applet will then execute the queries and 
     * retrieve the results as xml.
     * 
     * @param virtualPage The virtual page instance child.
     * @param form
     */
    protected void configureVirtualPage(HttpServletRequest request, HttpServletResponse response, String path, VirtualModel model, PathNodes pathNodes, Node virtualPageInstance, VirtualForm form) throws Exception {
        Node templateNode = virtualPageInstance.getProperty(Constants.PROP_TEMPLATE_NODE_REFERENCE).getNode();
        Node representation = templateNode.getNode(Constants.CHILD_REPRESENTATION);

        Node transformationNode = null;
        if (form.getMode().equals(Constants.PARVALUE_APPLET))
            transformationNode = representation.getNode(Constants.CHILD_EDITORTRANSFORMATION);
        else
            transformationNode = representation.getNode(Constants.CHILD_TRANSFORMATION);

        String query = virtualPageInstance.getProperty(Constants.PROP_PRIMARY_QUERY).getString();
        QueryParameters parameters = expandQuery(pathNodes, query);
        Value[] values = new Value[0];
        if ( virtualPageInstance.hasProperty(Constants.PROP_SECONDARY_QUERY))
            values = virtualPageInstance.getProperty(Constants.PROP_SECONDARY_QUERY).getValues();
        
        UpdatableSAXContentSource[] sources = new UpdatableSAXContentSource[values.length+1];
        String[] prefixes = new String[values.length+1];
        sources[0] = model.createRepositoryDataContentSource(parameters.getQuery(), parameters.getRoot());
        prefixes[0] = null;
        for( int i=1; i<sources.length; i++) {
            SecondaryQuery secondaryQuery = new SecondaryQuery(values[i-1].getString());
            SimpleQuery simple = new SimpleQuery(secondaryQuery.getQuery());
            
            prefixes[i] = DataModel.SECONDARY_QUERY.substring(1) + (secondaryQuery.getIndex()+1);            
            sources[i] = model.createRepositoryDataContentSource(secondaryQuery.getQuery(), simple.getPath());
        }
        
        String nodeTypeName = representation.getProperty(Constants.PROP_NODETYPE).getString();

        String html = model.createMappedHtml(transformationNode.getPath(), nodeTypeName, parentOf(parameters.getQuery()),
                model.createRepositoryDataContentSource(sources, prefixes), new HttpContext(getServlet(), request, response));
        
        form.setId(extractId(html));
        form.setHtml(new ByteArrayInputStream(html.getBytes()));
        
        VirtualAttribute att = new VirtualAttribute();
        att.setNodeId(extractId(html));
        att.setContent(html);
        att.setPath(path);
        if (representation.hasNode(Constants.CHILD_CSS))
            att.setCssPath(representation.getPath()+"/"+Constants.CHILD_CSS);
        att.setInstancePath(virtualPageInstance.getPath());
        request.setAttribute(VirtualAttribute.ATT_VIRTUAL, att);
    }
    
    /**
     * The last path component was associated to a virtual page.
     * 
     * From the template, retrieve the transformation and the child type information, add both to the form. From the instance, retrieve the primary query
     * and all secondary queries and add them to the form.
     * 
     * These will be used as applet parameters (secondary queries in the form of "secondary1", "secondary2" etc.). The applet will then execute the queries and 
     * retrieve the results as xml.
     * 
     * @param request 
     * @param model 
     * @param data 
     * @param virtualPageInstance 
     * @param form
     * @throws java.lang.Exception 
     */
    protected void configureVirtualPage(HttpServletRequest request, HttpServletResponse response, VirtualModel model, Node data, Node virtualPageInstance, VirtualForm form) throws Exception {
        Node templateNode = model.getReferencedNode(virtualPageInstance.getProperty(Constants.PROP_TEMPLATE_NODE_REFERENCE).getString());
        Node representation = templateNode.getNode(Constants.CHILD_REPRESENTATION);

        Node transformationNode = null;
        if (form.getMode().equals(Constants.PARVALUE_APPLET))
            transformationNode = representation.getNode(Constants.CHILD_EDITORTRANSFORMATION);
        else
            transformationNode = representation.getNode(Constants.CHILD_TRANSFORMATION);
      
        Value[] values = new Value[0];
        if ( virtualPageInstance.hasProperty(Constants.PROP_SECONDARY_QUERY))
            values = virtualPageInstance.getProperty(Constants.PROP_SECONDARY_QUERY).getValues();
        UpdatableSAXContentSource[] sources = new UpdatableSAXContentSource[values.length+1];
        String[] prefixes = new String[values.length+1];
        sources[0] = model.createNodeContentSource(data);
        prefixes[0] = null;
        for( int i=1; i<sources.length; i++) {
            String value = values[i-1].getString();

            SecondaryQuery secondaryQuery = new SecondaryQuery(values[i-1].getString());
            SimpleQuery simple = new SimpleQuery(secondaryQuery.getQuery());
            
            prefixes[i] = DataModel.SECONDARY_QUERY.substring(1) + (secondaryQuery.getIndex()+1);            
            sources[i] = model.createRepositoryDataContentSource(secondaryQuery.getQuery(), simple.getPath());
        }
        
        String nodeTypeName = representation.getProperty(Constants.PROP_NODETYPE).getString();

        String html = model.createMappedHtml(transformationNode.getPath(), nodeTypeName, data.getPath(),
                model.createRepositoryDataContentSource(sources, prefixes), new HttpContext(getServlet(), request, response));
        
        form.setId(extractId(html));
        form.setHtml(new ByteArrayInputStream(html.getBytes()));
        
        VirtualAttribute att = new VirtualAttribute();
        att.setNodeId(extractId(html));
        att.setContent(html);
        att.setPath(data.getPath());
        if (representation.hasNode(Constants.CHILD_CSS))
            att.setCssPath(representation.getPath()+"/"+Constants.CHILD_CSS);
        att.setInstancePath(virtualPageInstance.getPath());
        if (data.isNodeType(Constants.JCR_REFERENCEABLE)) {
            String virtualReference = "";
            virtualReference = HttpUtils.addParameter(virtualReference, HyperlinkModel.DATA_REFERENCE, data.getUUID());
            String templateReference = virtualPageInstance.getUUID();
            if (model.isMultipageNode(data))
                templateReference = getTemplateReference(request, form);
            virtualReference = HttpUtils.addParameter(virtualReference, VirtualHyperlinkModel.TEMPLATE_REFERENCE, templateReference);
            String type;
            if (request.getAttribute(WorkflowAttribute.ATT_WORKFLOW) != null || request.getSession().getAttribute(WorkflowAttribute.ATT_WORKFLOW) != null)
                type = Constants.CONTENT_TYPE_WORKFLOW;
            else
                type = Constants.CONTENT_TYPE_VIRTUAL;
            virtualReference = HttpUtils.addParameter(virtualReference, Constants.PAR_CONTENT_TYPE, type);
            att.setVirtualReference(virtualReference);
        }
        
        request.setAttribute(VirtualAttribute.ATT_VIRTUAL, att);
    }

    /**
     * Return the path as set by the dispatch action
     * 
     * @param request
     * @return
     */
    protected String getPath(HttpServletRequest request) {
        DispatchAttribute dispatch = (DispatchAttribute) request.getAttribute(DispatchAttribute.ATT_DISPATCH);
        if (dispatch == null)
            return null;

        return dispatch.getPath();
    }

    /**
     * Return the reference to the template.
     *
     * The reference has either been part of the request parameters ("vref=...") or it has been determined already in the
     * dispatch action. In the latter case, a VirtualDispatchAttribute has been added to the request.
     *
     * @param request
     * @param form
     * @return
     */
    protected String getTemplateReference(HttpServletRequest request, VirtualForm form) {
        String vref = null;

        if ( form.getRef() != null)
            vref = form.getVref();
        else if (request.getAttribute(VirtualDispatchAttribute.ATT_VIRTUAL_DISPATCH) != null) { // default template instance
            VirtualDispatchAttribute virtual = (VirtualDispatchAttribute) request.getAttribute(VirtualDispatchAttribute.ATT_VIRTUAL_DISPATCH);
            vref = virtual.getVref();
        }

        return vref;
    }
}
